
静态分析器是一个令人印象深刻的例子，说明了可以使用clang基础设施做些什么。也可以使用插件扩展clang，这样就可以将自己的功能添加到clang中。该技术非常类似于在LLVM中添加一个通道插件。

我们用一个简单的插件来探索这个功能，LLVM编码标准要求函数名以小写字母开头。然而，编码标准已经发生了变化，在许多情况下，函数以大写字母开头。一个警告违反命名规则的插件可以帮助解决这个问题，所以来试一试。

因为要在AST上运行用户定义的操作，所以需要定义PluginASTAction类的子类。若使用clang库编写自己的工具，则可以为操作定义ASTFrontendAction类的子类。PluginASTAction类是ASTFrontendAction类的子类，具有解析命令行选项的能力。

需要的另一个类是ASTConsumer类的子类。ASTConsumer是一个类，可以使用它在AST上运行一个动作，而不管AST的来源是什么。可以在NamingPlugin.cpp文件中创建实现:

\begin{enumerate}
\item
首先包括所需的头文件。除了上面提到的ASTConsumer类，还需要一个编译器和插件注册表的实例:

\begin{cpp}
#include "clang/AST/ASTConsumer.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
\end{cpp}

\item
使用clang命名空间，并将实现放在一个匿名命名空间中，以避免名称冲突:

\begin{cpp}
using namespace clang;
namespace {
\end{cpp}

\item
接下来，定义ASTConsumer类的子类。稍后，将希望在检测到违反命名规则的情况下发出警告，所以需要一个对DiagnosticsEngine实例的引用。

\item
需要在类中存储一个CompilerInstance实例，之后可以请求一个DiagnosticsEngine实例:

\begin{cpp}
class NamingASTConsumer : public ASTConsumer {
    CompilerInstance &CI;

public:
    NamingASTConsumer(CompilerInstance &CI) : CI(CI) {}
\end{cpp}

\item
ASTConsumer实例有几个入口方法，HandleTopLevelDecl()方法符合我们的目的，顶层为每个声明调用该方法。这不仅包括函数，还包括变量，所以必须使用LLVM RTTI dyn\_cast<>()函数来确定该声明是否为函数声明。HandleTopLevelDecl()方法有一个声明组作为参数，可以包含多个声明。这需要对声明进行循环。下面的代码显示了HandleTopLevelDecl()方法:

\begin{cpp}
    bool HandleTopLevelDecl(DeclGroupRef DG) override {
        for (DeclGroupRef::iterator I = DG.begin(),
                E = DG.end();
        I != E; ++I) {
            const Decl *D = *I;
            if (const FunctionDecl *FD =
                dyn_cast<FunctionDecl>(D)) {
\end{cpp}

\item
找到函数声明后，需要检索函数的名称。还需要确保名称不为空:

\begin{cpp}
                std::string Name =
                    FD->getNameInfo().getName().getAsString();
                assert(Name.length() > 0 &&
                    "Unexpected empty identifier");
\end{cpp}

若函数名不是以小写字母开头，就违反了命名规则:

\begin{cpp}
                char &First = Name.at(0);
                if (!(First >= 'a' && First <= 'z')) {
\end{cpp}

\item
要发出警告，需要一个DiagnosticsEngine实例，所以还需要一个消息ID。在clang内部，消息ID定义为枚举。因为插件不是clang的一部分，所以需要创建一个自定义ID，然后可以用它来发出警告:

\begin{cpp}
                    DiagnosticsEngine &Diag = CI.getDiagnostics();
                    unsigned ID = Diag.getCustomDiagID(
                        DiagnosticsEngine::Warning,
                        "Function name should start with "
                        "lowercase letter");
                    Diag.Report(FD->getLocation(), ID);
\end{cpp}

\item
除了关闭所有的大括号，需要从这个函数返回true来表示处理可以继续:

\begin{cpp}
                }
            }
        }
        return true;
    }
};
\end{cpp}

\item
接下来，需要创建PluginASTAction子类，它实现clang调用的接口:

\begin{cpp}
class PluginNamingAction : public PluginASTAction {
public:
\end{cpp}

必须实现的第一个方法是CreateASTConsumer()方法，其返回NamingASTConsumer类的一个实例。该方法由clang调用，传递的CompilerInstance实例，可以访问编译器的所有重要类:

\begin{cpp}
    std::unique_ptr<ASTConsumer>
    CreateASTConsumer(CompilerInstance &CI,
                      StringRef file) override {
        return std::make_unique<NamingASTConsumer>(CI);
    }
\end{cpp}

\item
插件还可以访问命令行选项。目前，插件没有命令行参数，只会返回true来表示成功:

\begin{cpp}
    bool ParseArgs(const CompilerInstance &CI,
                   const std::vector<std::string> &args)
                                                  override {
        return true;
    }
\end{cpp}

\item
插件的操作类型描述了何时调用该操作。默认值是Cmdline，则必须在要调用的命令行上命名插件。要重写该方法并将其值更改为AddAfterMainAction，它会自动运行该操作:

\begin{cpp}
    PluginASTAction::ActionType getActionType() override {
        return AddAfterMainAction;
    }
\end{cpp}

\item
PluginNamingAction类的实现完成了，只有类和匿名命名空间的右大括号不见了。将它们添加到代码中:

\begin{cpp}
};
}
\end{cpp}

\item
最后，需要注册插件。第一个参数是插件的名称，第二个参数是帮助信息:

\begin{cpp}
static FrontendPluginRegistry::Add<PluginNamingAction> X("naming-plugin",       "naming plugin");
\end{cpp}
\end{enumerate}

这就完成了插件的实现。要编译这个插件，在CMakeLists.txt文件中创建一个构建描述。该插件位于clang源代码树之外，需要设置一个完整的项目，可以按照以下步骤来做:

\begin{enumerate}
\item
首先定义所需的CMake版本和项目名称:

\begin{cmake}
cmake_minimum_required(VERSION 3.20.0)
project(naminglugin)
\end{cmake}

\item
接下来，包括LLVM文件。若CMake不能自动找到文件，必须设置LLVM\_DIR变量，使它指向包含CMake文件的LLVM目录:

\begin{cmake}
find_package(LLVM REQUIRED CONFIG)
\end{cmake}

\item
将包含CMake文件的LLVM目录附加到搜索路径中，并包含一些必需的模块:

\begin{cmake}
list(APPEND CMAKE_MODULE_PATH ${LLVM_DIR})
include(AddLLVM)
include(HandleLLVMOptions)
\end{cmake}

\item
然后，为clang加载CMake定义。若CMake不能自动找到文件，必须设置Clang\_DIR变量，使其指向包含CMake文件的Clang目录:

\begin{cmake}
find_package(Clang REQUIRED)
\end{cmake}

\item
接下来，定义头文件和库文件的位置，以及使用哪些定义:

\begin{cmake}
include_directories("${LLVM_INCLUDE_DIR}"
                    "${CLANG_INCLUDE_DIRS}")
add_definitions("${LLVM_DEFINITIONS}")
link_directories("${LLVM_LIBRARY_DIR}")
\end{cmake}

\item
前面的定义设置了构建环境。插入以下命令，定义了插件的名称，插件的源文件，并且是一个clang插件:

\begin{cmake}
add_llvm_library(NamingPlugin MODULE NamingPlugin.cpp
                 PLUGIN_TOOL clang)
\end{cmake}

Windows上，插件支持与Unix不同，必需的LLVM和clang库必须链接进来:

\begin{cmake}
if(WIN32 OR CYGWIN)
    set(LLVM_LINK_COMPONENTS Support)
    clang_target_link_libraries(NamingPlugin PRIVATE
        clangAST clangBasic clangFrontend clangLex)
endif()
\end{cmake}

\end{enumerate}

现在，可以配置和构建插件，假设设置了CMAKE\_GENERATOR和CMAKE\_BUILD\_TYPE环境变量:

\begin{shell}
$ cmake -DLLVM_DIR=~/LLVM/llvm-17/lib/cmake/llvm \
        -DClang_DIR=~/LLVM/llvm-17/lib/cmake/clang \
        -B build
$ cmake --build build
\end{shell}

这些步骤创建NamingPlugin，所以构建目录中的动态库。

要测试这个插件，将下面的源代码保存为named.c文件。函数名Func1违反了命名规则，但主函数不违反规则:

\begin{cpp}
int Func1() { return 0; }
int main() { return Func1(); }
\end{cpp}

要调用这个插件，需要指定-fplugin=选项:

\begin{shell}
$ clang -fplugin=build/NamingPlugin.so naming.c
naming.c:1:5: warning: Function name should start with lowercase
letter
int Func1() { return 0; }
    ^
1 warning generated.
\end{shell}

这种调用需要重写PluginASTAction类的getActionType()方法，并且返回一个不同于Cmdline默认值的值。

若没有这样做——例如，想对插件动作的调用有更多的控制——则可以从编译器命令行运行插件:

\begin{shell}
$ clang -cc1 -load ./NamingPlugin.so -plugin naming-plugin naming.c
\end{shell}

恭喜你——已经构建了你的第一个clang插件!这种方法的缺点是它有一定的局限性。ASTConsumer类有不同的入口方法，都是粗粒度的。这可以通过使用RecursiveASTVisitor类来解决。该类遍历所有AST节点，可以覆盖感兴趣的VisitXXX()方法。使用Visitor，可以按照以下步骤重写这个插件:

\begin{enumerate}
\item
需要一个附加的包含来定义RecursiveASTVisitor类。插入如下代码:

\begin{cpp}
#include "clang/AST/RecursiveASTVisitor.h"
\end{cpp}

\item
然后，将访问者定义为匿名命名空间中的第一个类。将只存储对AST上下文的引用，能够访问用于AST操作的所有重要方法，包括发出警告所需的DiagnosticsEngine实例:

\begin{cpp}
class NamingVisitor
    : public RecursiveASTVisitor<NamingVisitor> {
private:
    ASTContext &ASTCtx;
public:
    explicit NamingVisitor(CompilerInstance &CI)
        : ASTCtx(CI.getASTContext()) {}
\end{cpp}

\item
遍历期间，只要发现函数声明，就调用VisitFunctionDecl()方法。将内部循环的主体复制到HandleTopLevelDecl()函数中:

\begin{cpp}
    virtual bool VisitFunctionDecl(FunctionDecl *FD) {
        std::string Name =
            FD->getNameInfo().getName().getAsString();
        assert(Name.length() > 0 &&
                "Unexpected empty identifier");
        char &First = Name.at(0);
        if (!(First >= 'a' && First <= 'z')) {
            DiagnosticsEngine &Diag = ASTCtx.getDiagnostics();
            unsigned ID = Diag.getCustomDiagID(
                DiagnosticsEngine::Warning,
                "Function name should start with "
                "lowercase letter");
            Diag.Report(FD->getLocation(), ID);
        }
        return true;
    }
};
\end{cpp}

\item
这就完成了访问者的实现。NamingASTConsumer类中，只存储一个Visitor实例:

\begin{cpp}
    std::unique_ptr<NamingVisitor> Visitor;

public:
    NamingASTConsumer(CompilerInstance &CI)
        : Visitor(std::make_unique<NamingVisitor>(CI)) {}
\end{cpp}

\item
删除HandleTopLevelDecl()方法——该功能现在位于访问者类中，因此需要重写HandleTranslationUnit()方法。这个类对每个翻译单元调用一次，将从这里开始AST遍历:

\begin{cpp}
    void
    HandleTranslationUnit(ASTContext &ASTCtx) override {
        Visitor->TraverseDecl(
            ASTCtx.getTranslationUnitDecl());
    }
\end{cpp}

\end{enumerate}

这个新实现具有相同的功能，其优点是更容易扩展。例如，若想检查变量声明，必须实现VisitVarDecl()方法。若希望使用语句，则必须实现VisitStmt()方法。使用这种方法，可以为C、C++和Objective C语言的每个实体提供一个visitor方法。

通过访问AST，可以构建执行复杂任务的插件。如本节所述，强制命名约定是对clang的一个有用的补充。可以作为插件实现的另一个有用的附加功能是计算软件度量，例如圈复杂度。

还可以添加或替换AST节点，例如，添加运行时检测。添加插件可以以所需的方式扩展clang。





























